{
  // 让下面的代码成立(不能改动代码)
  var [a, b] = { a: 1, b: 2 };
  // 解析：
  // 在对象原型上添加可迭代属性即可
  // 考察点：迭代器 可迭代协议 原型 this Object.values
  Object.prototype[Symbol.iterator] = function () {
    return Object.values(this)[Symbol.iterator]();
  };
}

{
  // 能否以某种方式为上面的语句使用展开运算而不导致类型错误
  // 如果可以，写出解决方式
  var obj = { x: 1, y: 2, z: 3 };
  [...obj]; // TypeError

  // 解析
  // 展开语法和for-of 语句遍历iterabl对象定义要遍历的数据。
  // Array和Map是具有默认迭代行为的内置迭代器。
  // 对象不是可迭代的，但是可以通过使用iterable和iterator协议是它可迭代。
  // 如果一个对象实现了@iterator方法，那么他就是可迭代的，这意味着这个对象（或者它原型链上的对象）必须有一个带有@iterator键的属性，这个键可以通过Symbol.iterator获得】

  // 法一
  obj[Symbol.iterator] = function () {
    // iterator 是一个具有next方法的对象
    // 它的返回值至少有一个对象
    // 两个属性 value done

    return {
      // 返回一个 iterator 对象
      next: function () {
        if (this.count === 3) {
          return {
            value: this.count,
            done: true,
          };
        }

        this.count = this.count + 1;
        return {
          value: this.count,
          done: false,
        };
      },
      count: 0,
    };
  };
  [...obj];

  // 法二
  // 可以使用generator函数实现 定制对象的可迭代行为
  obj[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
  };
  [...obj];
}

{
  // 代码实现使得for..of 可以遍历下面对象
  const obj = { a: 1, b: 2, c: 3 };

  // 法一
  // 使用 Object.keys() Object.values() Object.entries()

  // 法二
  // 针对对象部署iterator接口
  obj[Symbol.iterator] = function () {
    let idx = 0;
    let _this = this;
    let keys = Object.keys(_this);
    let len = keys.length;

    return {
      next() {
        while (idx < len) {
          return {
            value: _this[keys[idx++]],
            done: false,
          };
        }
        return {
          value: null,
          done: true,
        };
      },
    };
  };

  // 法三
  // 使用 Generator 函数实现
  obj[Symbol.iterator] = function* () {
    const keys = Object.keys(this);
    for (let i = 0; i < keys.length; i++) {
      yield this[keys[i]];
    }
  };

  // for (const item of obj) {
  //   console.log(item);
  // }
}

{
  // 实现一个isNegtiveZero函数，只检查+0和-0，-0则返回true,+0返回false
  function isNegtiveZero(num) {
    if (num !== 0) {
      throw new RangeError('The argument must be +0 or -0');
    }

    return !Object.is(num, 0);
    return 1 / num === -Infinity;
  }
  // 注意将+0和-0转成字符串行不通
  // 因为 (+0).toString() (-0).toString() 直接转化成字符串0了不带符号了
  console.log(isNegtiveZero(+0));
  console.log(isNegtiveZero(-0));
}

{
  // 请实现一个 cacheRequest 方法，保证发出多次同一个 ajax 请求时都能拿到数据，而实际上只发出一次请求
  // 解析
  // cache构建Map，用作缓存数据，把url作为key，判断是否是同一个请求
  // 请求的其他参数可以传入options。如get，data等
  // 每次请求检查缓存，有则从缓存取，否则发起请求
  // 请求成功后，保存数据到cache并返回，失败则提示
  // 特殊情况，如果请求在pendding状态，则返回该请求继续等待
  // 请求用timeout代替

  const request = (url, option) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve({ data: option });
      }, 2000);
    });
  };

  const cache = new Map();

  const cacheRequest = (url, option = { method: 'GET' }) => {
    let key = `${url}:${option.method}`;

    if (cache.get(key)) {
      if (cache.get(key).status === 'pending') {
        return cache.get(key).myWait;
      }
      return Promise.resolve(cache.get(key).data);
    }

    // 无缓存，发请求
    let requestApi = request(url, option);
    cache.set(key, {
      status: 'pending',
      myWait: requestApi,
    });

    return requestApi
      .then((res) => {
        cache.set(key, {
          status: 'success',
          data: res,
        });
      })
      .catch((err) => {
        cache.set(key, {
          status: 'fail',
          data: err,
        });
        return Promise.reject(err);
      });
  };

  // 测试
  cacheRequest('url1').then((res) => console.log(res));
  cacheRequest('url1').then((res) => console.log(res));

  setTimeout(() => {
    cacheRequest('url1').then((res) => console.log(res));
  }, 4000);
}

{
  // 输入一个数字，找到对应的字母
  // 如输入1 返回 a
  // 输入26返回z
  // 输入27返回aa
  // 输入28返回ab
  // 输入53返回aaa

  function findLetterAccoringToNum(num) {
    // let strs = ''
    // // 65 => A 97 => a
    // for(let i = 65; i < 91; i ++) {
    //   strs += String.fromCharCode(i)
    // }

    let obj = {};
    for (let i = 1; i < 27; i++) {
      obj[i] = String.fromCharCode(i + 64);
    }

    let a = Math.floor(num / 26),
      b = num % 26;
    if (num <= 26) {
      return obj[num];
    } else {
      // let res = ''
      // for(let j = 0; j < a; j ++) {
      //   res += obj[1]
      // }
      // res += obj[b]
      // return res
      return (
        new Array(a).fill('').reduce((prev, cur) => prev + obj[1], '') + obj[b]
      );
    }
  }

  function findLetterAccoringToNum(num) {
    let str = '';
    let remainder = num % 26; // 求余数，决定结果的最后一个字母
    let quotient = Math.floor(num / 26); // 求商，一共循环了几轮字母表，决定结果除最后一位一共显示多少个‘a'

    // 如果remainder为0，说明最后一位是'z'，应该quotient-1，remainder = 26
    if (remainder === 0) {
      str += 'a';
      quotient--;
    }

    while (quotient > 0) {
      str += 'a';
      quotient--;
    }

    // 根据unicode编码求字母，如：a->97 = 1 + 96,z->122 = 26 + 96
    str += String.fromCharCode(remainder + 96);

    return str;
  }
  console.log(findLetterAccoringToNum(1));
  console.log(findLetterAccoringToNum(26));
  console.log(findLetterAccoringToNum(27));
  console.log(findLetterAccoringToNum(28));
  console.log(findLetterAccoringToNum(53));
}

{
  // 价格由低到高排序，价格相等时，按评分由高到低排序
  const list = [
    {
      id: 1,
      price: 100,
      score: 80,
    },
    {
      id: 2,
      price: 70,
      score: 90,
    },
    {
      id: 3,
      price: 70,
      score: 95,
    },
  ];

  function fn(list) {
    let temp = null;
    for (let i = 0; i < list.length - 1; i++) {
      for (let j = 0; j < list.length - 1 - i; j++) {
        if (list[j].price === list[j + 1].price) {
          if (list[j].score < list[j + 1].score) {
            temp = list[j];
            list[j] = list[j + 1];
            list[j + 1] = temp;
          }
        } else {
          if (list[j].price < list[j + 1].price) {
            temp = list[j];
            list[j] = list[j + 1];
            list[j + 1] = temp;
          }
        }
      }
    }
    return list;

    // sort 是快排 + 插入的结合体
    return list.sort((a, b) => {
      if (a.price === b.price) {
        return b.score - a.score;
      }
      return b.price - a.price;
    });
  }
  fn(list);
}

{
  // 有 100 瓶水，其中有一瓶有毒，小白鼠只要尝一点带毒的水 3 天后就会死亡，至少要多少只小白鼠才能在 3 天内鉴别出哪瓶水有毒？

  // 7 只
  // 每个老鼠只有死或活 2 种状态，因此每个老鼠可以看作一个 bit，取 0 或 1N 个老鼠可以看作 N 个 bit，可以表达 2^N 种状态（其中第 n 个状态代表第 n 个瓶子有毒）
  // 因此所有老鼠能表示的状态数能大于等于 100 即可。

  // 代码实现
  let n = 1;
  while (Math.pow(2, n) < 100) {
    n++;
  }
  console.log(n);
}

{
  /**
   * @param {*} whiteList 活跃用户
   * @param {*} participant 抽奖用户
   */
  function lottery(whiteList, participant) {
    const pLen = participant.length, // 抽奖用户数
      wLen = whiteList.length, // 白名单用户数
      targetNum = 20000; // 总中奖用户
    if (pLen === 0) return [];
    // 如果参与中奖人数小于总中奖人数，那就全部中奖
    if (pLen < targetNum) return participant;
    let res = [], // 中奖名单
      i = 0;
    const map = new Map();
    // 白名单用户处理
    while (i < wLen && res.length <= targetNum) {
      //   从抽奖用户查找白名单用户
      const pIndex = participant.indexOf(whiteList[i]);
      if (pIndex !== -1) {
        map.set(pIndex, true);
        res.push(whiteList[i]);
      }
      i++;
    }
    //  普通抽奖用户处理
    while (res.length < targetNum) {
      //  随机抽取普通用户
      const index = Math.floor(Math.random() * pLen);
      // 如果是白名单用户就跳过
      if (map.has(index)) continue;
      res.push(participant[index]);
      map.set(index, true);
    }
    return res;
  }

  function lottery(whiteList, participant) {
    // 中奖用户
    const count = 20000;
    // 中奖名单
    const res = [];
    whiteList.sort();
    participant.sort();

    // 随机获取抽奖用户
    const pickRandom = () => {
      const idx = parseInt(Math.random() * participant.length);
      res.push(participant.splice(idx, 1)[0]);
    };
    // 统计白名单人数
    let i1 = 0;
    // 统计中奖人数
    let i2 = 0;
    // 当中奖名单小于中奖人数
    while (res.length < count) {
      // 先从白名单中抽取
      if (i1 < whiteList.length && i2 < participant.length) {
        // 查找抽奖用户中的白名单用户
        if (whiteList[i1] === participant[i2]) {
          let [ele] = participant.splice(i2, 1);
          res.push(ele);
          i1++;
        } else if (whiteList[i1] > participant[i2]) {
          i2++;
        } else {
          i1++;
        }
      } else {
        // 如果白名单人数抽完了，开始从普通用户中抽取
        pickRandom();
      }
    }
    return res;
  }

  // 测试
  const whiteList = Array.from({ length: 10000 }, (v, k) => '' + k);
  const participant = Array.from({ length: 100000 }, (v, k) => '' + k);
  console.log(lottery(whiteList, participant));
}

{
  // 使用队列来实现击鼓传花

  // 击鼓传花的规则:
  // 几个朋友一起玩一个游戏, 围成一圈, 开始数数（数数时数字都是挨着的，第一个人数 1，之后的人数 2）,
  // 数到某个数字的人自动淘汰。汰的人后面再从 1 开始数，重复上面的游戏，直到就剩下一个人，请问剩下了谁?

  // 队列的特征（队尾插入，队头删除）,这里可以使用内置的 push、shift 方法
  // 思路：用到对列的数据结构，整个队伍放在一个队列中，当某一个人数完之后就把他放在队列的末尾

  function passGame(names, num) {
    while (names.length > 1) {
      // 内层循环将要删除的人放在队尾，所以我们只需要在队尾删除这个人即可
      for (let i = 0; i < num; i++) {
        // 队头的人数数，数完之后放到队尾
        names.push(names.shift());
      }
      names.pop();
    }
    return names.pop();
  }

  function passGame(names, num) {
    let total = 0;
    let endName = null;

    while (names.length > 1) {
      total++;
      if (total % num === 0) {
        if (names.length === 1) {
          endName = names.pop();
          break;
        }
      } else {
        names.push(names.shift());
      }
    }
    return endName;
  }

  // 测试例子
  var names = ['John', 'Jack', 'Camila', 'Ingrid', 'Carl'];
  var endName = passGame(names, 8); // 数到 8 的人淘汰
  console.log('最终留下:' + endName); // 'john'
}

{
  // 设计一个支持两种操作的数据结构
  // addWord(word); // 添加字符串
  // search(word); // 返回布尔值，是否存在
  // search可以搜索文字或正则表达式字符串，字符串只包含字母 . 或 a-z 。
  // . 可以表示任何一个字母。
  // 示例
  // addWord("bad")
  // addWord("dad")
  // addWord("mad")
  // search("pad") -> false
  // search("bad") -> true
  // search(".ad") -> true
  // search("b..") -> true
  // 可以假设所有单词都是由小写字母 a-z 组成的。
  // 解析：
  // 这道题要求字符串既可以被添加、又可以被搜索，这就意味着字符串在添加时一定要被存在某处。键值对存储，可以用 Map(或对象字面量来模拟 Map).
  // 注意：为了降低查找时的复杂度，我们可以考虑以字符串的长度为 key，相同长度的字符串存在一个数组中，这样可以提高后续定位的效率
  function WordDictionary() {
    this.words = [];
  }
  WordDictionary.prototype.addWord = function (word) {
    const len = word.length;
    if (this.words[len]) {
      this.words[len].push(word);
    } else {
      this.words[len] = [word];
    }
  };
  WordDictionary.prototype.search = function (word) {
    const len = word.length;
    // 若该字符串长度在Map中对应的数组长度不存在，则该字符串不存在
    if (!this.words[len]) {
      return false;
    }
    // 判断是否存在.,不包含一定是普通字符串
    if (!word.includes('.')) {
      // 定位到和目标字符穿长度一致的字符串数组，查找是否存在
      return this.words[len].includes(word);
    }
    // 否则是正则表达式
    const reg = new RegExp(word);
    return this.words[len].some((item) => reg.test(item));
  };

  const wordDictionary = new WordDictionary();

  wordDictionary.addWord('bad');
  wordDictionary.addWord('dad');
  wordDictionary.addWord('mad');
  console.log(wordDictionary.search('pad'));
  console.log(wordDictionary.search('bad'));
  console.log(wordDictionary.search('.ad'));
  console.log(wordDictionary.search('b..'));
}

{
  // 完成一个转换函数，将数字转换成对应的大写字母，满足下面的对应关系
  // 1 => A
  // 2 => B
  // 3 => C
  // ...
  // 26 => Z
  // 27 => AA
  // 28 => AB
  // ...
  // 52 => AZ
  // 53 => BA
  // 54 => BB
  // 大写字母 A - Z 对应的 ACSII 码值是 65 - 90
  function convert(n) {
    let res = '';
    while (n) {
      let m = n % 26;
      n = Math.floor(n / 26);
      if (m === 0) {
        m = 26;
        n = n - 1;
      }
      res = String.fromCharCode(m + 64) + res;
    }
    return res;
  }
  console.log(convert(1));
  console.log(convert(2));
  console.log(convert(3));
  console.log(convert(26));
  console.log(convert(27));
  console.log(convert(28));
  console.log(convert(52));
  console.log(convert(53));
  console.log(convert(54));
}

{
  // 输入两个字符串 2018-08 2018-12 输出他们中间的月份 [2018-10, 2018-11]
  {
    let getMiddleMonth = function (str1, str2) {
      let [year1, month1] = str1.split('-'),
        [year2, month2] = str2.split('-');
      year1 = Number(year1);
      month1 = Number(month1);
      year2 = Number(year2);
      month2 = Number(month2);

      // 间隔月数（包含开始和终止）
      let len = (year2 - year1) * 12 + (month2 - month1);

      let temp,
        result = [],
        start = new Date(str1),
        end = new Date(str2);

      // 月份字符串映射
      let monthMaps = new Array(12)
        .fill('')
        .map((v, idx) => {
          return idx + 1 > 9 ? `${idx + 1}` : `0${idx + 1}`;
        })
        .reduce((lastRes, item, idx) => {
          lastRes[idx] = item;
          return lastRes;
        }, {});

      for (var i = 1; i < len - 1; i++) {
        // 从开始计算
        temp = new Date(str1);
        // setMonth 13为明年的第二个月
        temp.setMonth(start.getMonth() + i + 1);
        result.push(`${temp.getFullYear()}-${monthMaps[temp.getMonth()]}`);
      }
      return result;
    };
    console.log(getMiddleMonth('2018-08', '2019-08'));
    const getMonths = (startDateStr, endDateStr) => {
      let startTime = getDate(startDateStr).getTime();
      const endTime = getDate(endDateStr).getTime();
      const result = [];
      while (startTime < endTime) {
        let curDate = new Date(startTime);
        result.push(formatDate(curDate));
        curDate.setMonth(curDate.getMonth() + 1);
        startTime = curDate.getTime();
      }
      return result.slice(1);
    };
    const getDate = (dateStr) => {
      const [year, month] = dateStr.split('-');
      return new Date(year, month - 1);
    };
    const formatDate = (date) => {
      return `${date.getFullYear()}-${(date.getMonth() + 1)
        .toString()
        .padStart(2, '0')}`;
    };
    const getMonths = (startDateStr, endDateStr) => {
      let startTime = getDate(startDateStr).getTime();
      const endTime = getDate(endDateStr).getTime();
      const result = [];
      while (startTime < endTime) {
        let curDate = new Date(startTime);
        result.push(formatDate(curDate));
        curDate.setMonth(curDate.getMonth() + 1);
        startTime = curDate.getTime();
      }
      return result.slice(1);
    };
    const getDate = (dateStr) => {
      const [year, month] = dateStr.split('-');
      return new Date(year, month - 1);
    };
    const formatDate = (date) => {
      return `${date.getFullYear()}-${(date.getMonth() + 1)
        .toString()
        .padStart(2, '0')}`;
    };
    console.log(getMonths('2018-08', '2018-12'));
  }
}

{
  // 递归
  let setter = function (content, key, value) {
    const argArr = key.split('.');
    let i = argArr.shift();
    if (argArr.length === 0) {
      content[i] = value;
    } else {
      content[i] = setter(content[i], argArr.join('.'), value);
    }
    return content;
  };

  // 利用reduce 对key层层递进
  let setter = function (content, key, value) {
    let keyArr = key.split('.');
    let len = keyArr.length;
    keyArr.reduce((lastRes, item, idx) => {
      if (idx === len - 1) {
        // 对key拆分的数组的最后一项赋值即可
        return (lastRes[item] = value);
      }
      return lastRes[item];
    }, content);
  };
  let n = {
    a: {
      b: {
        c: { d: 1 },
        bx: { y: 1 },
      },
      ax: { y: 1 },
    },
  };
  // 修改值
  setter(n, 'a.b.c.d', 3);
  console.log(n.a.b.c.d); // 3
  setter(n, 'a.b.bx', 1);
  console.log(n.a.b.bx); // 1
}

{
  // 手写实现 sleep 函数
  // 法一 promise实现
  function sleep(ts) {
    return new Promise((resolve) => setTimeout(resolve, ts));
  }
  sleep(1000).then(() => {
    console.log(1);
  });
  // es5实现
  function sleep(cb, ts) {
    if (typeof cb === 'function') {
      setTimeout(cb, ts);
    }
  }
  sleep(() => console.log(2), 1000);
}

{
  // 实现一个打点计时器
  /*
  1.从start至end,每隔100毫秒console.log一个数字，每次数字增幅为1
  2.返回的对象中需要包含一个cancel方法，用于停止定时操作
  3.第一个数字需要立即输出
  */

  // 法一 简单直接
  const count = (start, end) => {
    console.log(start);
    let timer = setInterval(() => {
      if (start < end) {
        console.log((start += 1));
      }
    }, 100);

    return {
      cancel: () => {
        clearInterval(timer);
      },
    };
  };
  const fn = count(1, 100);
  fn.cancel();

  // 法二 完善一些
  function tapper(start, end) {
    if (start > end || Number.isNaN(Number(start)) || Number.isNaN(Number(end)))
      throw new Error('invalid input');
    const interval = 100;
    let isCancel = false;
    const init = async () => {
      console.info(start);
      start++;
      while (start <= end && !isCancel) {
        await new Promise((resolve) => {
          setTimeout(() => {
            if (!isCancel) {
              console.info(start);
              start++;
            }
            resolve();
          }, interval);
        });
      }
    };

    const cancel = () => {
      isCancel = true;
    };

    return {
      init,
      cancel,
    };
  }

  const demo = tapper(1, 100);
  demo.init();
  setTimeout(() => {
    demo.cancel();
  }, 2000);
}

{
  // 找出数组中出现最多的数字
  const arr = [3, 5, 6, 6, 6, 6, 5, 9, 8, 10, 5, 7];
  function findMostRepetitionsNumber(arr) {
    let num = null;
    let count = 1;
    let o = {};
    for (let i = 0; i < arr.length; i++) {
      if (!o[arr[i]]) {
        o[arr[i]] = 1;
      } else {
        o[arr[i]] += 1;
        if (o[arr[i]] > count) {
          count++;
          num = arr[i];
        }
      }
    }

    return {
      num,
      count,
    };
  }
  console.log(findMostRepetitionsNumber(arr));
}

{
  // 使用ES6 的Proxy实现数组负索引
  // 例如，可以简单地使用arr[-1]替代arr[arr.length-1]访问最后一个元素，[-2]访问倒数第二个元素，以此类推
  const proxyArray = (arr) => {
    const len = arr.length;
    return new Proxy(arr, {
      get(target, key, receiver) {
        key = +key % len; // 对key超过数组长度做处理
        if (key < 0) {
          key += len;
        }

        return target[key];
      },
    });
  };
  var a = proxyArray([1, 2, 3, 4, 5, 6, 7, 8, 9]);
  console.log(a[1]); // 2
  console.log(a[-1]); // 9
  console.log(a[-2]); // 8
  console.log(a[-10]); // 9
  console.log(a[-20]); // 8
}
